Original Note

Data Wrangling(整理版) - Read

Data Wrangling(整理版)

原始资料: Missing Semester - Data Wrangling created: 2026-07-01 11:19 整理说明: 本版本结合原始笔记和可读取的原始教程重排、补全和翻译,保留常用英文术语;原始笔记未修改。

内容简要概括

Data Wrangling 指把原始数据整理成更适合阅读、统计或继续处理的形式,最常见的方式是用 shell pipeline 把多个小工具组合起来。本文以 SSH 日志为例,串联 sshgrepsedsortuniqawkpastexargs,展示如何从大量日志中抽取用户名、统计频次并生成参数列表。核心思路是:先过滤,再抽取,再排序统计,最后按需要格式化输出或交给下一个命令执行。

Data Wranglingpipelinegrepsedawkregular expressioncapture groupsortuniqpastexargsjournalctlssh log

目录


1. grep、sed、awk 常用方式与参数速查

1.1 grep: 按行过滤文本

grep 用来从输入中筛选匹配某个 pattern 的行。它最适合做 pipeline 的早期过滤,把大量文本先缩小到相关行。

常用形式:

grep PATTERN file
command | grep PATTERN
grep -i "error" app.log
grep -v "debug" app.log
grep -E "error|warning" app.log
grep -F "[literal text]" app.log

常用参数:

参数 含义 典型用途
-i ignore case,忽略大小写 同时匹配 ErrorerrorERROR
-v invert match,反向匹配 排除包含某个 pattern 的行
-E extended regexp,启用扩展正则 使用 +?、`
-F fixed string,把 pattern 当普通字符串 不希望 .[ 等字符被当成正则
-n 显示行号 定位配置或日志位置
-c 只输出匹配行数 快速统计
-r / -R 递归搜索目录 在项目目录中查找文本
-A N after,显示匹配行之后 N 行 查看日志上下文
-B N before,显示匹配行之前 N 行 查看错误前发生了什么
-C N context,显示前后各 N 行 同时查看上下文

常见组合:

# 找出 sshd 相关日志
journalctl | grep sshd

# 找出包含 Disconnected from 的 sshd 日志
journalctl | grep sshd | grep "Disconnected from"

# 排除某类 nightly toolchain
rustup toolchain list | grep nightly | grep -vE "nightly-x86"

1.2 sed: 流式替换、删除与抽取

sed 是 stream editor,擅长对每一行文本执行替换、删除、打印等操作。最常见命令是 s/REGEX/SUBSTITUTION/,即把匹配 REGEX 的部分替换成 SUBSTITUTION

常用形式:

sed 's/old/new/' file
command | sed 's/old/new/g'
command | sed -E 's/(user|admin): (.*)/\2/'
sed -n '1,20p' file
sed '/DEBUG/d' file

常用参数与命令:

写法 含义 典型用途
s/A/B/ 每行只替换第一个 AB 简单替换
s/A/B/g 每行替换所有 AB 全局替换
s/A// 每行删除第一个 A 删除匹配文本
s/A//g 每行删除所有 A 批量清理
-E 使用 extended regular expression 少写反斜杠,便于使用 ()?+
-n suppress automatic printing 搭配 p 只打印指定内容
p print,打印匹配或指定行 sed -n '1,10p' file
d delete,删除匹配行 sed '/DEBUG/d' file
-i in-place edit,原地修改文件 修改前要确认备份策略

sed 的替换命令可以用捕获组保留想要的部分:

echo "user: alice" | sed -E 's/user: (.*)/\1/'

输出:

alice

1.3 awk: 按字段处理文本流

awk 是一门适合处理文本流的小型编程语言。它默认按空白字符把每一行切成字段,$0 表示整行,$1$2、... 表示第 1、第 2 个字段。

常用形式:

awk '{print $2}' file
command | awk '{print $1, $2}'
command | awk -F, '{print $1}'
command | awk '$1 > 10 {print $2}'
command | awk 'BEGIN {sum = 0} {sum += $1} END {print sum}'

常用参数与内置变量:

写法 含义 典型用途
'{print $2}' 打印每行第 2 个字段 从统计结果中取用户名
-F SEP 设置 field separator 处理 CSV、冒号分隔日志等
$0 当前整行 打印或匹配完整行
$1, $2 当前行的第 1、第 2 个字段 提取列
NF number of fields 获取字段数,$NF 是最后一列
NR number of records 当前行号
BEGIN 输入开始前执行 初始化变量或打印表头
END 输入结束后执行 输出累计结果
~ /REGEX/ 字段匹配正则 awk '$2 ~ /^c.*e$/'
&&, ` `

awk 的基本结构是:

pattern { action }

如果省略 pattern,默认匹配所有行;如果省略 action,默认打印整行。

2. Data Wrangling 的基本思路

Data Wrangling 的核心不是某一个工具,而是把许多小工具串成 pipeline:

  1. grep 先筛掉无关数据。
  2. sedawk 抽取需要的字段。
  3. sortuniqwc 等工具做统计。
  4. pastexargs 或其他命令把结果变成后续命令需要的格式。

只要使用了 | 把一个命令的输出交给另一个命令继续处理,就已经在做某种形式的 Data Wrangling。例如:

journalctl | grep -i intel

这条命令把完整系统日志转换成“只包含 intel 的日志行”。看起来只是搜索,实质上已经把原始数据变成了更有用的子集。

3. 从远程 SSH 日志开始过滤

3.1 在本地过滤远程日志

下面这条命令在远程服务器执行 journalctl,把完整日志传回本地,再在本地用 grep 过滤:

ssh myserver journalctl | grep sshd

问题是远程日志可能非常大。这样会把所有日志都传回本地,即使最后只需要其中很少一部分,网络传输和本地处理都会比较慢。

3.2 在远程先过滤,再传回本地

更好的方式是在远程服务器上完成主要过滤,再把结果传回本地:

ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less

这里的单引号很重要:被引号包住的整段 pipeline 会在远程服务器执行。这样本地只接收已经过滤过的日志,再用 less 分页查看。

命令拆解:

片段 作用
ssh myserver '...' 登录远程服务器并执行引号内的命令
journalctl 读取系统日志
grep sshd 保留 SSH daemon 相关日志
grep "Disconnected from" 进一步保留断开连接相关日志
less 本地分页查看长输出

3.3 保存过滤结果,方便本地调试

调试 pipeline 时,可以先把远程过滤结果保存成文件,避免每次都访问网络:

ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
less ssh.log

> 表示重定向输出,把命令结果写入 ssh.log。后续可以直接对 ssh.logsedawksort 等实验。

4. sed 与 regular expression

4.1 删除固定前缀

原始日志中有很多前缀信息。如果只想保留 Disconnected from 后面的内容,可以写:

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed 's/.*Disconnected from //'

sed 's/.*Disconnected from //' 的含义是:

部分 含义
s/.../.../ substitution,替换命令
.* 任意字符重复 0 次或多次
Disconnected from 要匹配的字面文本
第二个 / 后为空 替换为空,也就是删除匹配内容

因此,它会把每行中 Disconnected from 及其之前的内容删掉,只留下后面的部分。

4.2 常见 regular expression 元字符

写法 含义
. 匹配除 newline 以外的任意单个字符
* 前一个匹配重复 0 次或多次
+ 前一个匹配重复 1 次或多次
[abc] 匹配 abc 中任意一个字符
[^ ] 匹配任意非空格字符
`(RX1 RX2)`
? 前一个匹配出现 0 次或 1 次
^ 行首
$ 行尾
[0-9]+ 一个或多个数字

sed 默认使用 basic regular expression,部分元字符需要反斜杠才能生效。加上 -E 后使用 extended regular expression,写复杂正则会更自然。

4.3 贪婪匹配与 perl 的非贪婪写法

*+ 默认是 greedy,也就是尽可能多地匹配文本。比如用户名本身叫 Disconnected from 时:

Jan 17 03:13:00 example.com sshd[2631]: Disconnected from invalid user Disconnected from 46.97.239.16 port 55920 [preauth]

简单写法:

sed 's/.*Disconnected from //'

可能会匹配到最后一个 Disconnected from,导致用户名被吃掉,只剩下 IP 和端口。某些正则引擎支持 .*? 表示非贪婪匹配,但 sed 不支持这种写法。可以用 perl

perl -pe 's/.*?Disconnected from //'

不过本教程继续使用更常见的 sed,通过匹配整行和捕获组来解决问题。

4.4 匹配整行并删除整行内容

为了避免用户名中的 Disconnected from 干扰,可以匹配完整日志结构:

sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'

正则拆解:

片段 含义
.*Disconnected from 匹配日志开头直到 Disconnected from
`(invalid authenticating )?`
user 匹配字面文本 user
.* 匹配用户名部分
[^ ]+ 匹配一个空格后的一段非空格文本,通常是 IP
port [0-9]+ 匹配端口字段
( \[preauth\])? 可选匹配 [preauth],方括号需要转义
$ 匹配行尾
替换为空 删除整行匹配内容

这个版本能识别整条日志,但因为替换结果为空,所以会把匹配到的行变成空行。

4.5 使用捕获组提取用户名

真正需要的是保留用户名。把用户名部分放进捕获组,再在替换结果中引用它:

sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

关键点:

片段 含义
`(invalid authenticating )?`
(.*) 第 2 个捕获组,保存用户名
\2 在替换结果中引用第 2 个捕获组

完整 pipeline:

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

输出会变成一行一个用户名,后续就可以继续排序和统计。

5. 提取用户名并统计频次

5.1 sort 与 uniq -c

先得到用户名列表,再统计每个用户名出现了多少次:

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
  | sort \
  | uniq -c

sort 会把输入按字典序排序。uniq -c 会合并相邻的重复行,并在前面加上出现次数。

必须先 sort 的原因是:uniq 只合并相邻重复行。例如:

root
admin
root

直接 uniq -c 无法把两个 root 合并。排序后:

admin
root
root

再运行 uniq -c,相同用户名就相邻了。

5.2 按次数排序并取前 10 个

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
  | sort \
  | uniq -c \
  | sort -nk1,1 \
  | tail -n10

命令拆解:

片段 含义
sort 把相同用户名排到一起
uniq -c 统计每个用户名出现次数
sort -n 按数字大小排序,而不是按字符串排序
sort -k1,1 只使用第 1 列作为排序键
sort -nk1,1 -n-k1,1 的组合写法
tail -n10 取最后 10 行,即出现次数最多的 10 个

如果想看最少见的用户名,可以用 head 代替 tail。如果想降序排序,可以使用 sort -r

5.3 生成逗号分隔的用户名列表

有时需要把一行一个用户名转换成逗号分隔的列表,例如写入配置文件:

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
  | sort \
  | uniq -c \
  | sort -nk1,1 \
  | tail -n10 \
  | awk '{print $2}' \
  | paste -sd,

命令拆解:

片段 含义
awk '{print $2}' uniq -c 的结果中取第 2 列,也就是用户名
paste -s serial,把多行合并成一行
paste -d, delimiter 使用逗号
paste -sd, 合并成一行,并用逗号分隔

在 macOS 上,系统自带的是 BSD paste,和 GNU paste 在某些行为上可能不同。如果遇到兼容问题,可以安装 GNU coreutils 或改用其他方式生成列表。

6. awk 处理字段与条件

6.1 打印指定字段

awk '{print $2}' 的意思是:对每一行,打印第 2 个字段。

在下面这种输入中:

12 root
3 admin
1 test

命令:

awk '{print $2}'

输出:

root
admin
test

字段默认由空白字符分隔。如果数据是逗号分隔,可以用 -F,

awk -F, '{print $2}' users.csv

6.2 使用 pattern 过滤行

awk 程序可以写成 pattern { action }。例如统计只出现 1 次、并且用户名以 c 开头、以 e 结尾的数量:

awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l

拆解:

片段 含义
$1 == 1 第 1 列等于 1,也就是出现次数为 1
&& 逻辑与,两个条件都要满足
$2 ~ /^c[^ ]*e$/ 第 2 列匹配正则:以 c 开头,以 e 结尾
{ print $2 } 打印第 2 列用户名
wc -l 统计输出行数

6.3 使用 BEGIN 和 END 做累计

awk 不只是字段抽取工具,也可以保存变量并做简单计算:

BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }

含义:

部分 作用
BEGIN { rows = 0 } 读取输入前初始化变量
条件块 对满足条件的行,把第 1 列累加到 rows
END { print rows } 输入结束后输出累计结果

理论上,awk 可以替代前面部分 grepsed 的工作,但拆成多个小工具通常更容易学习、调试和解释。

7. Data Wrangling to Make Arguments

数据整理工具配合 xargs 很有用,因为 xargs 可以把前面 pipeline 的输出变成后面命令的参数。

示例:卸载旧的 Rust nightly toolchain。

rustup toolchain list \
  | grep nightly \
  | grep -vE "nightly-x86" \
  | sed 's/-x86.*//' \
  | xargs rustup toolchain uninstall

命令拆解:

片段 含义
rustup toolchain list 列出已安装的 Rust toolchain
grep nightly 保留 nightly 版本
grep -vE "nightly-x86" 排除当前想保留的 nightly-x86 相关版本
sed 's/-x86.*//' 删除 -x86 及其后面的部分,只留下 toolchain 名称前缀
xargs rustup toolchain uninstall 把输入中的 toolchain 名称作为参数传给卸载命令

xargs 常见参数:

参数 含义 典型用途
-n N 每次传 N 个参数给后续命令 控制批处理粒度
-I {} {} 作为占位符 把输入插入命令中间
-0 使用 null 字符作为分隔符 搭配 find -print0 安全处理带空格文件名
-r 没有输入时不执行命令 GNU xargs 常用,避免误执行

涉及删除、卸载、移动文件等操作时,建议先把 xargs 后面的命令换成 echo 检查参数:

rustup toolchain list \
  | grep nightly \
  | grep -vE "nightly-x86" \
  | sed 's/-x86.*//' \
  | xargs echo rustup toolchain uninstall

确认输出无误后,再执行真正的卸载命令。

8. Wrangling Binary Data

前面主要处理文本数据,但 pipeline 对 binary data 也同样有用。原始教程给出的例子是:从摄像头抓取一帧图像,转成灰度,压缩,通过 SSH 传到远程机器,在远程解压、保存副本并显示。

ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 - \
  | convert - -colorspace gray - \
  | gzip \
  | ssh mymachine 'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -'

命令拆解:

片段 含义
ffmpeg ... -f image2 - 从摄像头读取一帧图像,并把图像写到 stdout
convert - -colorspace gray - 从 stdin 读取图像,转换为灰度,再写到 stdout
gzip 压缩二进制数据流
ssh mymachine '...' 把压缩后的数据流传到远程机器处理
gzip -d 在远程解压
tee copy.jpg 保存一份副本,同时继续把数据传给后续命令
env DISPLAY=:0 feh - 在远程图形界面中显示图像

这说明 shell pipeline 不只适用于文本;只要命令能从 stdin 读取、向 stdout 写入,就可以组合处理文本或二进制流。

9. 可复用命令模板汇总

9.1 远程过滤日志并本地查看

ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less

9.2 保存远程过滤结果

ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
less ssh.log

9.3 从 SSH 日志提取用户名

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

9.4 统计出现最多的 10 个用户名

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
  | sort \
  | uniq -c \
  | sort -nk1,1 \
  | tail -n10

9.5 输出逗号分隔的用户名列表

ssh myserver journalctl \
  | grep sshd \
  | grep "Disconnected from" \
  | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
  | sort \
  | uniq -c \
  | sort -nk1,1 \
  | tail -n10 \
  | awk '{print $2}' \
  | paste -sd,

9.6 用 xargs 把整理结果变成命令参数

rustup toolchain list \
  | grep nightly \
  | grep -vE "nightly-x86" \
  | sed 's/-x86.*//' \
  | xargs rustup toolchain uninstall